Skip to content

feat: receive payments directly without Lightning Address#61

Open
Delgado74 wants to merge 4 commits intolachispame:mainfrom
Delgado74:main
Open

feat: receive payments directly without Lightning Address#61
Delgado74 wants to merge 4 commits intolachispame:mainfrom
Delgado74:main

Conversation

@Delgado74
Copy link

@Delgado74 Delgado74 commented Mar 21, 2026

Summary

Allow users to receive Lightning payments without requiring a Lightning Address by creating invoices directly from the wallet.

Changes

  • New feature: Generate invoices directly from the app to receive payments without needing a Lightning Address
  • Added support for clearing and copying invoices
  • Improved localization with new translated strings
  • Applied CodeRabbit recommendations for code quality improvements

Motivation

Currently, users need a Lightning Address to receive payments. This change enables anyone with a LaChispa wallet to receive payments by simply generating an invoice, making the app more accessible and easier to use for new users.

Testing

  • Manual testing of invoice generation
  • Verified localization strings in all supported languages (EN, ES, DE, FR, IT, PT, RU)

Summary by CodeRabbit

  • New Features

    • Enhanced invoice creation and management on the receive screen: amount input, QR display, copy/clear actions, and safer timeout/validation handling.
    • Added localized text for invoice/Lightning address flows in DE, EN, ES, FR, IT, PT, RU.
  • Chores

    • Added automated image build-and-publish release workflow and adjusted release trigger to include image references.
    • Updated SDK and key dependencies for compatibility.
  • Documentation

    • Minor Docker docs anchor/link adjustment and Nginx proxy client-IP handling added.

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

Adds a Docker build-and-push workflow, updates release workflow to depend on it and include GHCR image info, modifies Docker runtime and nginx config, enhances the receive screen with invoice UI/logic and parsing, adds invoice-related localizations across 7 languages, and bumps SDK/dependency constraints.

Changes

Cohort / File(s) Summary
CI / Release Workflows
.github/workflows/docker-release.yml, .github/workflows/release.yml
Added Docker build-and-push workflow (tags v*) publishing to ghcr.io; adjusted release.yml to run after the docker workflow and include GHCR image pull/run instructions and derive VERSION from workflow_run head branch.
Docker image & nginx
docker/Dockerfile, docker/nginx.conf, docker/README.md
Build stage uses BuildKit cache for flutter pub get; switched runtime base to nginxinc/nginx-unprivileged:alpine, exposed port 8080, simplified healthcheck. nginx trusts X-Forwarded-For from internal CIDRs and enables real_ip_recursive. Spanish README anchor updated.
Receive screen (UI & logic)
lib/screens/9receive_screen.dart
Major UI and logic changes: added _isCheckingInvoice guard, 10-minute invoice timeout timer, safe dispose/cancel logic, localized-number normalization helper, improved currency init with error handling, invoice-only card (QR, memo, copy/clear), LN address CTA, and modal-driven invoice creation with validation and snackbars.
Localization
lib/l10n/app_*.arb (de, en, es, fr, it, pt, ru), l10n.yaml
Added invoice / Lightning-address related keys across languages: create_lnaddress_label, create_invoice_label, invoice_empty_response_error, invoice_cleared_message, clear_invoice_button, invoice_copied_message, and invoice_amount_label (with placeholder). l10n.yaml newline fixed.
Dependencies
pubspec.yaml
Raised Dart SDK lower bound to >=3.3.0, upgraded intl to ^0.20.2, bumped webview_flutter to ^4.13.1, and added webview_flutter_android: ^4.10.13.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as Receive Screen
    participant State as App State
    participant Currency as Currency Service
    participant Invoice as Invoice Generator

    User->>UI: Open receive screen / request amount
    UI->>State: initialize currencies
    State->>Currency: fetch server & rates
    Currency-->>State: currencies (or default)
    State-->>UI: update UI

    User->>UI: enter amount & create invoice
    UI->>UI: check _isCheckingInvoice guard
    UI->>UI: normalize localized number
    UI->>Invoice: generate invoice (amount)
    Invoice-->>UI: payment request / QR
    UI->>UI: start 10-min timeout timer
    UI->>User: render invoice card (QR, memo, copy/clear)

    User->>UI: copy or clear invoice
    UI->>UI: cancel timer, clear invoice, show confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through Docker builds tonight,
QR codes glowing, invoices bright.
In many tongues I tap and sing,
A Lightning hop — behold the zing! ⚡️✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature: enabling direct invoice-based payment reception without requiring a Lightning Address, which aligns with the PR's primary objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docker/nginx.conf (1)

36-38: ⚠️ Potential issue | 🔴 Critical

Port mismatch: listen 80 is incompatible with nginx-unprivileged.

The Dockerfile was changed to use nginxinc/nginx-unprivileged:alpine, which runs as a non-root user and cannot bind to privileged ports (below 1024). This config still specifies listen 80, which will cause the container to fail on startup.

🐛 Proposed fix: Update to port 8080
     server {
-        listen 80;
+        listen 8080;
         server_name localhost;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/nginx.conf` around lines 36 - 38, The nginx config's server block
currently uses the privileged port via the "listen 80;" directive which is
incompatible with the nginx-unprivileged image; update the server block's listen
directive from 80 to 8080 (i.e., change "listen 80;" to "listen 8080;") so the
container can bind as a non-root user when using the nginx-unprivileged image.
lib/screens/9receive_screen.dart (1)

1972-2047: ⚠️ Potential issue | 🟠 Major

Guard the paid flow against stale polling results.

Canceling the timer does not cancel an in-flight checkInvoiceStatus() call. If the user clears or replaces the invoice while that await is running, this callback can still navigate away for the old payment hash because the result is never revalidated against the current _generatedInvoice.

Suggested guard
   void _startInvoicePaymentMonitoring(
     LightningInvoice invoice,
     WalletInfo wallet,
     String serverUrl,
   ) {
+    final monitoredHash = invoice.paymentHash;
+
     _invoicePaymentTimer = Timer.periodic(const Duration(seconds: 2), (
       timer,
     ) async {
       if (_isCheckingInvoice) {
         return;
@@
         final isPaid = await _invoiceService.checkInvoiceStatus(
           serverUrl: serverUrl,
           adminKey: wallet.adminKey,
-          paymentHash: invoice.paymentHash,
+          paymentHash: monitoredHash,
         );
+
+        if (!mounted || _generatedInvoice?.paymentHash != monitoredHash) {
+          timer.cancel();
+          return;
+        }

         if (isPaid) {
           print('[RECEIVE_SCREEN] Invoice paid! Starting celebration sequence');
           timer.cancel();
+          _invoicePaymentTimeoutTimer?.cancel();

           if (mounted) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/9receive_screen.dart` around lines 1972 - 2047, The paid-flow can
act on stale results because the awaited _invoiceService.checkInvoiceStatus call
isn't revalidated against the current invoice; capture the current
invoice/paymentHash into a local variable (e.g., localInvoice or
requestedPaymentHash) before calling _invoiceService.checkInvoiceStatus, then
after awaiting isPaid verify that the current _generatedInvoice still matches
that local identifier (compare paymentHash or object identity) and only proceed
to cancel _invoicePaymentTimer, trigger
_transactionDetector.triggerEventSpark('invoice_paid'), navigate, and show the
SnackBar if they match; if they don't match, ignore the result and return,
making sure to still reset _isCheckingInvoice in finally.
🧹 Nitpick comments (2)
.github/workflows/docker-release.yml (1)

34-37: latest tag will never be applied for tag-triggered builds.

The condition enable={{is_default_branch}} evaluates to false for tag push events because tags don't have a branch context. If you want latest to track the most recent release tag, consider using a different approach.

♻️ Option to always apply `latest` for version tags
           tags: |
             type=ref,event=tag
             type=sha,prefix=
-            type=raw,value=latest,enable={{is_default_branch}}
+            type=raw,value=latest

Alternatively, if you want latest only on stable releases (not pre-releases), you could use a more complex condition with a separate step.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker-release.yml around lines 34 - 37, The current tags
entry "type=raw,value=latest,enable={{is_default_branch}}" never becomes true
for tag-triggered builds; update the tags logic so the "latest" tag is applied
for the default branch and/or for tag pushes. Concretely, modify the tags block
that contains "type=raw,value=latest,enable={{is_default_branch}}" so its enable
expression also checks for tag refs (e.g., enable={{ startsWith(github.ref,
'refs/tags/') || is_default_branch }}), or split into two tag entries: one
conditioned on is_default_branch and another conditioned on
startsWith(github.ref,'refs/tags/'), ensuring the symbol "type=raw,value=latest"
is used for the tag push case.
lib/screens/9receive_screen.dart (1)

74-78: Avoid refreshing exchange rates through both paths.

CurrencySettingsProvider.updateServerUrl() already reloads currencies and exchange rates in lib/providers/currency_settings_provider.dart:359-372. When the server changes, the extra loadExchangeRates(forceRefresh: true) immediately repeats that work and slows screen initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/9receive_screen.dart` around lines 74 - 78, The screen is
redundantly forcing exchange-rate reload after calling
CurrencySettingsProvider.updateServerUrl which already reloads currencies and
rates; remove the extra await currencyProvider.loadExchangeRates(forceRefresh:
true) call in the 9receive_screen (the block checking
authProvider.currentServer) so the updateServerUrl(...) call alone handles
reloading and avoids duplicate work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 77-80: The documentation's docker run command maps container port
80 but the Dockerfile and base image (nginx-unprivileged) expose port 8080;
update the docker run port mapping in the release workflow snippet by changing
the container side from :80 to :8080 (i.e., modify the docker run command
referenced in the diff to use -p 7777:8080) so the published port matches the
container's exposed port.
- Around line 3-7: The workflow_run trigger is using a branches filter that will
never match tag-triggered runs; remove the branches: [main] filter from the
workflow_run block so the trigger can fire for the referenced workflow, and
update the workflows reference to use the target workflow's name ("Build and
Push Docker Image") instead of the filename ("docker-release.yml"); specifically
modify the on.workflow_run section (workflows and branches keys) to only list
the workflow name and remove the branches key.

In `@docker/Dockerfile`:
- Around line 16-24: Update all references to the old port 80 to the new 8080 to
match the Docker image EXPOSE change: in docker/nginx.conf change the server
listen directive (e.g., "listen 80") to "listen 8080"; in docker-compose.yml
update the service port mapping (e.g., "7777:80") to "7777:8080", change the
NGINX_PORT env value from 80 to 8080, and update any Traefik label
loadbalancer.server.port values from 80 to 8080; in docker/README.md update any
documented Traefik label examples (loadbalancer.server.port=80) to 8080; and in
the GitHub Actions workflow replace docker run -p 7777:80 with docker run -p
7777:8080 so all runtime, docs, and labels are consistent.

In `@docker/README.md`:
- Line 191: Replace the obsolete anchor using the name attribute (<a
name="espanol-section"></a>) with an HTML element that uses an id attribute
(e.g., <a id="espanol-section"></a> or add id to the target heading) so markdown
renderers and GitHub link resolution work reliably; update any internal links
that referenced "#espanol-section" if needed to point to the same id, and prefer
adding the id to the section heading rather than a standalone empty anchor for
clarity.

In `@lib/l10n/app_it.arb`:
- Line 71: The localized string for key "create_lnaddress_label" is unidiomatic
Italian; replace the value with a native-speaker–approved CTA (for example an
idiomatic alternative such as "oppure crea un indirizzo:" or "oppure crea un:"),
ensuring tone/punctuation match the rest of the Italian file and confirm the
final phrasing with a native Italian reviewer before merging.

In `@lib/l10n/app_ru.arb`:
- Line 71: The localized string for key "create_lnaddress_label" is awkward and
has an extra space before the colon; update the value in app_ru.arb to a
natural, grammatically correct Russian CTA without the stray space (for example
rephrase to something like "или вы также можете создать адрес:" or another
idiomatic short CTA) so the text reads fluently and the punctuation is correct.

In `@lib/screens/9receive_screen.dart`:
- Around line 1736-1738: The hardcoded Spanish exception thrown when
amountInSats exceeds the 21M limit should be replaced with a localized message
from the app's l10n resources instead of inline text; in the guard where you
check amountInSats (> 2100000000000000) (around the code that currently throws
Exception('Monto muy grande. Máximo: 21M BTC')), replace that throw with one
that uses the localization API (e.g.,
AppLocalizations.of(context).maxAmountError or the project's equivalent key) or
throw a typed error carrying a localization key so the catch path can call the
l10n lookup before showing the snackbar. Ensure you reference the same unique
symbols (amountInSats and the throwing location in 9receive_screen /
ReceiveScreen) so the message is not language-locked.
- Around line 115-142: The current validation uses double.tryParse on the raw
controller text which rejects comma-decimal inputs before
_normalizeLocalizedNumber can run; update the validation logic that currently
parses controller.text directly to instead call
_normalizeLocalizedNumber(controller.text.trim()) and treat a non-null return as
valid (use the returned double value for further checks and storage), replacing
any direct double.tryParse(controller.text) checks so inputs like "1,5" or "1
000,50" are accepted.

---

Outside diff comments:
In `@docker/nginx.conf`:
- Around line 36-38: The nginx config's server block currently uses the
privileged port via the "listen 80;" directive which is incompatible with the
nginx-unprivileged image; update the server block's listen directive from 80 to
8080 (i.e., change "listen 80;" to "listen 8080;") so the container can bind as
a non-root user when using the nginx-unprivileged image.

In `@lib/screens/9receive_screen.dart`:
- Around line 1972-2047: The paid-flow can act on stale results because the
awaited _invoiceService.checkInvoiceStatus call isn't revalidated against the
current invoice; capture the current invoice/paymentHash into a local variable
(e.g., localInvoice or requestedPaymentHash) before calling
_invoiceService.checkInvoiceStatus, then after awaiting isPaid verify that the
current _generatedInvoice still matches that local identifier (compare
paymentHash or object identity) and only proceed to cancel _invoicePaymentTimer,
trigger _transactionDetector.triggerEventSpark('invoice_paid'), navigate, and
show the SnackBar if they match; if they don't match, ignore the result and
return, making sure to still reset _isCheckingInvoice in finally.

---

Nitpick comments:
In @.github/workflows/docker-release.yml:
- Around line 34-37: The current tags entry
"type=raw,value=latest,enable={{is_default_branch}}" never becomes true for
tag-triggered builds; update the tags logic so the "latest" tag is applied for
the default branch and/or for tag pushes. Concretely, modify the tags block that
contains "type=raw,value=latest,enable={{is_default_branch}}" so its enable
expression also checks for tag refs (e.g., enable={{ startsWith(github.ref,
'refs/tags/') || is_default_branch }}), or split into two tag entries: one
conditioned on is_default_branch and another conditioned on
startsWith(github.ref,'refs/tags/'), ensuring the symbol "type=raw,value=latest"
is used for the tag push case.

In `@lib/screens/9receive_screen.dart`:
- Around line 74-78: The screen is redundantly forcing exchange-rate reload
after calling CurrencySettingsProvider.updateServerUrl which already reloads
currencies and rates; remove the extra await
currencyProvider.loadExchangeRates(forceRefresh: true) call in the
9receive_screen (the block checking authProvider.currentServer) so the
updateServerUrl(...) call alone handles reloading and avoids duplicate work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 407cf2df-d830-4bd1-b1aa-5827eb6d67d5

📥 Commits

Reviewing files that changed from the base of the PR and between 9a8aa2f and ef31419.

⛔ Files ignored due to path filters (8)
  • lib/l10n/generated/app_localizations.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_de.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_en.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_es.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_fr.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_it.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_pt.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_ru.dart is excluded by !**/generated/**
📒 Files selected for processing (15)
  • .github/workflows/docker-release.yml
  • .github/workflows/release.yml
  • docker/Dockerfile
  • docker/README.md
  • docker/nginx.conf
  • l10n.yaml
  • lib/l10n/app_de.arb
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_fr.arb
  • lib/l10n/app_it.arb
  • lib/l10n/app_pt.arb
  • lib/l10n/app_ru.arb
  • lib/screens/9receive_screen.dart
  • pubspec.yaml

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/l10n/app_it.arb`:
- Line 159: The ARB entry "invoice_amount_label" includes a {amount} placeholder
but is missing its metadata; add a corresponding metadata block named
"@invoice_amount_label" that documents the message (brief description) and
defines the placeholders map with "amount" (specify its type, e.g. "String" or
"double" as used in source locales, and an example/format if applicable). Repeat
the same metadata addition for the other affected files (app_de.arb, app_fr.arb,
app_pt.arb, app_ru.arb) to match en/es and restore correct localization type
generation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2215ab1f-da9e-4434-8c48-c655a320fb96

📥 Commits

Reviewing files that changed from the base of the PR and between ef31419 and 2f61e47.

⛔ Files ignored due to path filters (2)
  • lib/l10n/generated/app_localizations_it.dart is excluded by !**/generated/**
  • lib/l10n/generated/app_localizations_ru.dart is excluded by !**/generated/**
📒 Files selected for processing (2)
  • lib/l10n/app_it.arb
  • lib/l10n/app_ru.arb
✅ Files skipped from review due to trivial changes (1)
  • lib/l10n/app_ru.arb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant